﻿using Microsoft.Office.InfoPath;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Windows.Forms;
using System.Xml;
using System.Xml.XPath;

namespace InfoPathHelpers.Attachments
{
    /// <summary>
    /// Provides the logic for the Attachment Control template part.
    /// </summary>
    public class AttachmentsControl
    {
        const int BYTE_TO_MEGABYTE_FACTOR = 1024 * 1024;
        const string FILE_XPATH = "my:Files/my:File";

        protected FormContext _FormContext;
        protected XPathNavigator _RootNode;
        protected IXmlNamespaceResolver _NamespaceManager;
        OpenFileDialog _OpenDialog;
        FolderBrowserDialog _SaveAllDialog;
        SaveFileDialog _SaveDialog;
        bool _ValidateBeforeAdding;
        string _Prefix;
        string _Namespace;

        /// <summary>
        /// Creates a new instance of the AttachmentControl class using the default attachments control node.
        /// </summary>
        /// <param name="form">A reference to the form.</param>
        public AttachmentsControl(FormContext formContext)
            : this(formContext.SelectSingleNode("//my:Attachments"), formContext.NamespaceManager)
        {
            _FormContext = formContext;
        }

        /// <summary>
        /// Creates a new instance of the AttachmentControl class using the specified attachments control node.
        /// </summary>
        /// <param name="form">A reference to the form.</param>
        /// <param name="attachmentsRootXPath"></param>
        public AttachmentsControl(FormContext formContext, string attachmentsRootXPath)
            : this(formContext.SelectSingleNode(attachmentsRootXPath), formContext.NamespaceManager) 
        {
            _FormContext = formContext;
        }

        /// <summary>
        /// Creates a new instance of the AttachmentControl class.
        /// </summary>
        /// <param name="attachmentsNode"></param>
        /// <param name="namespaceManager"></param>
        public AttachmentsControl(XPathNavigator attachmentsNode, IXmlNamespaceResolver namespaceManager)
        {
            if (attachmentsNode == null)
            {
                throw new ArgumentNullException("attachmentsNode", "Attachments node not found.");
            }

            _NamespaceManager = namespaceManager;
            _RootNode = attachmentsNode;
            _Namespace = _RootNode.NamespaceURI;
            _Prefix = _RootNode.Prefix;
            _OpenDialog = new OpenFileDialog();
            _SaveDialog = new SaveFileDialog();
            _SaveAllDialog = new FolderBrowserDialog();
            AdditionalFields = new Dictionary<string, object>();
            OverwriteOnSave = true;
        }

        protected string Prefix
        {
            get { return _Prefix; }
        }

        protected string Namespace
        {
            get { return _Namespace; }
        }

        public string Filter
        {
            get { return _OpenDialog.Filter; }
            set { _OpenDialog.Filter = value; }
        }

        public List<string> AllowedExtensions
        {
            get 
            {
                var AllowedExtensions = new List<string>();
                var FilterTypes = Filter.Split('|');
                for(var i = 1; i < FilterTypes.Length; i+=2)
                {
                    var Extensions = FilterTypes[i].Split(';');
                    foreach (var Extension in Extensions)
                    {
                        // Remove the *. that is required as part of the filter.
                        var CleanExtension = Extension.Replace("*", string.Empty).Replace(".", string.Empty).ToLower();
                        if(!string.IsNullOrEmpty(CleanExtension) && !AllowedExtensions.Contains(CleanExtension))
                        {
                            AllowedExtensions.Add(CleanExtension);
                        }
                    }
                }

                return AllowedExtensions;
            }
        }

        public string InitialAddDirectory
        {
            get { return _OpenDialog.InitialDirectory; }
            set { _OpenDialog.InitialDirectory = value; }
        }

        public string InitialSaveDirectory
        {
            get { return _SaveDialog.InitialDirectory; }
            set { _SaveDialog.InitialDirectory = _SaveAllDialog.SelectedPath = value; }
        }

        public bool OverwriteOnSave { get; set; }

        public bool IsMultiselect
        {
            get { return _OpenDialog.Multiselect; }
            set { _OpenDialog.Multiselect = value; }
        }

        public bool AllowDuplicateFileNames { get; set; }

        public bool ValidateBeforeAdding
        {
            get { return _ValidateBeforeAdding; }
            set { _ValidateBeforeAdding = true; }
        }

        public double MaxAttachmentSize
        {
            get { return double.Parse(_RootNode.SelectSingleNode("my:MaxAttachmentSizeMb", _NamespaceManager).Value); }
            set { _RootNode.SelectSingleNode("my:MaxAttachmentSizeMb", _NamespaceManager).SetValue(value.ToString()); }
        }

        public int MaxNumberOfAttachments
        {
            get { return int.Parse(_RootNode.SelectSingleNode("my:MaxAttachments", _NamespaceManager).Value); }
            set { _RootNode.SelectSingleNode("my:MaxAttachments", _NamespaceManager).SetValue(value.ToString()); }
        }

        public double MaxTotalAttachmentSize
        {
            get { return double.Parse(_RootNode.SelectSingleNode("my:MaxTotalAttachmentSizeMb", _NamespaceManager).Value); }
            set { _RootNode.SelectSingleNode("my:MaxTotalAttachmentSizeMb", _NamespaceManager).SetValue(value.ToString()); }
        }

        public double TotalSize
        {
            get { return (double)_RootNode.Evaluate("sum(my:Files/my:File/my:FileSize)", _NamespaceManager); }
        }

        private int Count
        {
            get { return _RootNode.Select(FILE_XPATH, _NamespaceManager).Count; }
        }

        protected virtual List<Attachment> SelectedAttachments
        {
            get
            {
                List<Attachment> Attachments = new List<Attachment>();
                foreach (XPathNavigator Attachment in _RootNode
                    .Select("my:Files/my:File[./my:FileIsSelected = 'true']", _NamespaceManager))
                {
                    Attachments.Add(new Attachment(Attachment, _NamespaceManager));
                }

                return Attachments;
            }
        }

        protected virtual XPathNodeIterator IsSelectedNodes
        {
            get { return _RootNode.Select("my:Files/my:File/my:FileIsSelected", _NamespaceManager); }
        }

        protected virtual List<Attachment> Attachments
        {
            get
            {
                List<Attachment> Attachments = new List<Attachment>();
                foreach (XPathNavigator Attachment in _RootNode
                    .Select(FILE_XPATH, _NamespaceManager))
                {
                    Attachments.Add(new Attachment(Attachment, _NamespaceManager));
                }

                return Attachments;
            }
        }

        public IDictionary<string, object> AdditionalFields { get; private set; }

        public virtual void AddHandler(object sender, ClickedEventArgs e)
        {
            if (ValidateBeforeAdding && MaxNumberOfAttachments > 0 && Count >= MaxNumberOfAttachments)
            {
                MessageBox.Show("The maximum allowed number of attachments (" + MaxNumberOfAttachments + ") has been reached",
                    "Maximum Reached", MessageBoxButtons.OK, MessageBoxIcon.Warning);
                return;
            }

            if (_OpenDialog.ShowDialog() == DialogResult.Cancel)
            {
                return;
            }

            foreach (string FileName in _OpenDialog.FileNames)
            {
                var ExistingFile = _RootNode.SelectSingleNode($"my:Files/my:File[my:FileName = '{FileName}']", _NamespaceManager);

                if (ExistingFile != null && !AllowDuplicateFileNames)
                {
                    if (MessageBox.Show("An attachment with the same name has already been added. Would you like to replace it?",
                        "Microsoft InfoPath", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) == DialogResult.No)
                    {
                        // Duplicate name detected, user chose not to replace.
                        return;
                    }
                    else
                    {
                        // Delete the existing file and continue adding new file.
                        ExistingFile.DeleteSelf();
                    }
                }
                

                AddAttachment(FileName);
            }
        }

        public void AddAttachment(string fileName)
        {
            FileInfo File = new FileInfo(fileName);

            if (!IsValidExtension(File.Extension))
            {
                MessageBox.Show("\"" + File.Name + "\" could not be attached" + System.Environment.NewLine +
                    "You must select a file of the following type(s): " + string.Join("; ", AllowedExtensions.ToArray()),
                    "Microsoft InfoPath", MessageBoxButtons.OK, MessageBoxIcon.Warning);
                return;
            }

            if (ValidateBeforeAdding)
            {
                double FileSizeMb = (double)File.Length / BYTE_TO_MEGABYTE_FACTOR;
                if (MaxNumberOfAttachments > 0 && Count >= MaxNumberOfAttachments)
                {
                    MessageBox.Show("The maximum allowed number of attachments " +
                        "(" + MaxNumberOfAttachments + ") has been reached",
                        "Maximum Reached", MessageBoxButtons.OK, MessageBoxIcon.Warning);
                    return;
                }
                else if (MaxAttachmentSize > 0 && FileSizeMb > MaxAttachmentSize)
                {
                    MessageBox.Show(File.Name + " (" + FileSizeMb.ToString("0.00") + "Mb) exceeds the allowed maximum " +
                        "attachment size (" + MaxAttachmentSize.ToString("0.00") + "Mb).",
                        "File Too Large", MessageBoxButtons.OK, MessageBoxIcon.Warning);
                    return;
                }
                else if (MaxTotalAttachmentSize > 0 && TotalSize + FileSizeMb > MaxTotalAttachmentSize)
                {
                    MessageBox.Show("Adding " + File.Name + " (" + FileSizeMb.ToString("0.00") + "Mb) will exceed the allowed total " +
                        "size of attachments (" + MaxTotalAttachmentSize.ToString("0.00") + "Mb).",
                        "Maximum Reached", MessageBoxButtons.OK, MessageBoxIcon.Warning);
                    return;
                }
            }

            using (XmlWriter Writer = _RootNode.SelectSingleNode("my:Files", _NamespaceManager).AppendChild())
            {
                Writer.WriteStartElement(Prefix, "File", Namespace);
                WriteAttachment(Writer, File);
                Writer.WriteEndElement();
            }
        }

        /// <summary>
        /// Writes the individual elements to the File group. Can be used to add additional fields if overridden.
        /// </summary>
        protected virtual void WriteAttachment(XmlWriter writer, FileInfo file)
        {
            
            writer.WriteElementString(Prefix, "FileName", Namespace, file.Name);
            writer.WriteElementString(Prefix, "FileFullName", Namespace, file.FullName);
            writer.WriteElementString(Prefix, "FileSize", Namespace, ((double)file.Length / BYTE_TO_MEGABYTE_FACTOR).ToString());
            writer.WriteElementString(Prefix, "FileLastModified", Namespace, file.LastWriteTimeUtc.ToString("s"));
            writer.WriteElementString(Prefix, "FileData", Namespace, AttachmentEncoder.EncodeAttachment(file));
            writer.WriteElementString(Prefix, "FileIsSelected", Namespace, "false");
            writer.WriteElementString(Prefix, "FileValidationHelper", Namespace, "");

            foreach(var AdditionalField in AdditionalFields)
            {
                writer.WriteElementString(Prefix, AdditionalField.Key, Namespace, NodeOperations.Convert(AdditionalField.Value, false));
            }
            
        }

        public virtual void RemoveHandler(object sender, ClickedEventArgs e)
        {
            foreach (Attachment Attachment in SelectedAttachments)
            {
                Attachment.DeleteSelf();
            }
        }

        public virtual void OpenHandler(object sender, ClickedEventArgs e)
        {
            foreach (Attachment Attachment in SelectedAttachments)
            {
                ProcessStartInfo AttachmentStartInfo = new ProcessStartInfo();
                AttachmentStartInfo.UseShellExecute = true;

                if (Attachment.FileExists())
                {
                    AttachmentStartInfo.FileName = Attachment.FullName;
                }
                else
                {
                    AttachmentStartInfo.FileName = AttachmentEncoder.DecodeAttachment(Attachment.Data).FullName;
                }

                Process.Start(AttachmentStartInfo);
            }
        }

        public virtual void SaveHandler(object sender, ClickedEventArgs e)
        {
            foreach (Attachment Attachment in SelectedAttachments)
            {
                if (_SaveDialog.ShowDialog() != DialogResult.OK)
                {
                    continue;
                }

                AttachmentEncoder.DecodeAttachment(Attachment.Data, _SaveDialog.FileName, OverwriteOnSave);
            }
        }

        public virtual void SaveAllHandler(object sender, ClickedEventArgs e)
        {
            if (_SaveAllDialog.ShowDialog() != DialogResult.OK)
            {
                return;
            }

            string RootDirectory = _SaveAllDialog.SelectedPath;

            foreach (Attachment Attachment in Attachments)
            {
                string SavePath = Path.Combine(RootDirectory, Attachment.FileName);
                AttachmentEncoder.DecodeAttachment(Attachment.Data, SavePath, OverwriteOnSave);
            }
        }

        /// <summary>
        /// Saves all attachments to the provided directory.
        /// Note: If the directory does not exist it will be created.
        /// </summary>
        /// <param name="directory"></param>
        public void SaveAll(string directory)
        {
            if (!Directory.Exists(directory))
            {
                Directory.CreateDirectory(directory);
            }

            foreach (Attachment Attachment in Attachments)
            {
                string SavePath = Path.Combine(directory, Attachment.FileName);
                AttachmentEncoder.DecodeAttachment(Attachment.Data, SavePath, OverwriteOnSave);
            }
        }

        /// <summary>
        /// Refreshes the RootNode of the AttachmentsControl.
        /// Note: If the AttachmentsControl was constructed without FormContext the overloaded method must be used.
        /// </summary>
        public void Refresh()
        {
            if (_FormContext == null)
            {
                throw new InvalidOperationException("The AttachmentsControl was not constructed with a FormContext, please use the overloaded Refresh method.");
            }
            _RootNode = _FormContext.SelectSingleNode("//" + _RootNode.Name);

        }

        /// <summary>
        /// Oveload to refresh the RootNode of the AttachmentsControl if not constructed with FormContext.
        /// </summary>
        /// <param name="attachmentsNode">An XPathNavigator pointing to the attachments element of the form.</param>
        public void Refresh(XPathNavigator attachmentsNode)
        {
            _RootNode = attachmentsNode;
        }

        /// <summary>
        /// Clears all attachments.
        /// </summary>
        public void Clear()
        {
            string EmptyNode = string.Format("<{0}:Files xmlns:{0}=\"{1}\"/>", _Prefix, _Namespace);
            _RootNode.SelectSingleNode("my:Files", _NamespaceManager).ReplaceSelf(EmptyNode);
        }

        public bool IsValidExtension(string extension)
        {
            List<string> ValidExtensions = AllowedExtensions;
            return ValidExtensions.Count == 0 || AllowedExtensions.Contains(extension.Replace(".",string.Empty).ToLower());
        }

        protected class Attachment
        {
            protected XPathNavigator _AttachmentNode;
            protected IXmlNamespaceResolver _NamespaceManager;

            public Attachment(XPathNavigator attachmentNode, IXmlNamespaceResolver namespaceManager)
            {
                _AttachmentNode = attachmentNode;
                _NamespaceManager = namespaceManager;
            }

            public string FileName
            {
                get
                {
                    if (_AttachmentNode == null)
                    {
                        throw new InvalidOperationException("Attachment no longer exists");
                    }
                    return _AttachmentNode.SelectSingleNode("my:FileName", _NamespaceManager).Value;

                }
                set
                {
                    if (_AttachmentNode == null)
                    {
                        throw new InvalidOperationException("Attachment no longer exists");
                    }
                    _AttachmentNode.SelectSingleNode("my:FileName", _NamespaceManager).SetValue(value);


                }
            }

            public string FullName
            {
                get
                {
                    if (_AttachmentNode == null)
                    {
                        throw new InvalidOperationException("Attachment no longer exists");
                    }
                    return _AttachmentNode.SelectSingleNode("my:FileFullName", _NamespaceManager).Value;

                }
                set
                {
                    if (_AttachmentNode == null)
                    {
                        throw new InvalidOperationException("Attachment no longer exists");
                    }
                    _AttachmentNode.SelectSingleNode("my:FileFullName", _NamespaceManager).SetValue(value);

                }
            }

            public int Size
            {
                get
                {
                    if (_AttachmentNode == null)
                    {
                        throw new InvalidOperationException("Attachment no longer exists");
                    }
                    return int.Parse(_AttachmentNode.SelectSingleNode("my:FileSize", _NamespaceManager).Value);

                }
                set
                {
                    if (_AttachmentNode == null)
                    {
                        throw new InvalidOperationException("Attachment no longer exists");
                    }
                    _AttachmentNode.SelectSingleNode("my:FileSize", _NamespaceManager).SetValue(value.ToString());

                }
            }

            public DateTime LastModified
            {
                get
                {
                    if (_AttachmentNode == null)
                    {
                        throw new InvalidOperationException("Attachment no longer exists");
                    }
                    return DateTime.Parse(_AttachmentNode.SelectSingleNode("my:FileLastModified", _NamespaceManager).Value);

                }
                set
                {
                    if (_AttachmentNode == null)
                    {
                        throw new InvalidOperationException("Attachment no longer exists");
                    }
                    _AttachmentNode.SelectSingleNode("my:FileLastModified", _NamespaceManager).SetValue(value.ToString("s"));

                }
            }

            public string Data
            {
                get
                {
                    if (_AttachmentNode == null)
                    {
                        throw new InvalidOperationException("Attachment no longer exists");
                    }
                    return _AttachmentNode.SelectSingleNode("my:FileData", _NamespaceManager).Value;
                }
                set
                {
                    if (_AttachmentNode == null)
                    {
                        throw new InvalidOperationException("Attachment no longer exists");
                    }
                    _AttachmentNode.SelectSingleNode("my:FileData", _NamespaceManager).SetValue(value);

                }
            }

            public bool IsSelected
            {
                get
                {
                    if (_AttachmentNode == null)
                    {
                        throw new InvalidOperationException("Attachment no longer exists");
                    }
                    return bool.Parse(_AttachmentNode.SelectSingleNode("my:FileIsSelected", _NamespaceManager).Value);

                }
                set
                {
                    if (_AttachmentNode == null)
                    {
                        throw new InvalidOperationException("Attachment no longer exists");
                    }
                    _AttachmentNode.SelectSingleNode("my:FileIsSelected", _NamespaceManager).SetValue(value.ToString().ToLower());

                }
            }

            public bool FileExists()
            {
                return (File.Exists(FullName));
            }

            public void DeleteSelf()
            {
                _AttachmentNode.DeleteSelf();
                _AttachmentNode = null;
            }

            public void DeleteLocal()
            {
                if (FileExists())
                {
                    try
                    {
                        File.Delete(FullName);
                    }
                    catch (UnauthorizedAccessException) { }
                }
            }
        }
    }
}
